package name.zhengkeli.cloud.auth

import kotlinx.coroutines.Dispatchers
import kotlinx.datetime.Clock
import name.zhengkeli.cloud.auth.AuthSchema.MetadataTable
import name.zhengkeli.cloud.auth.AuthSchema.PasswordsTable
import name.zhengkeli.cloud.auth.AuthSchema.UsernamesTable
import name.zhengkeli.cloud.auth.AuthSchema.UsersTable
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

object AuthSchema {
    const val VERSION = "0.1.4"

    object MetadataTable : UUIDTable("metadata") {
        val key = text("key")
        val value = text("value")
        val createTime = timestamp("create_time")
        val deleteTime = timestamp("delete_time").nullable()

        init {
            uniqueIndex(key, filterCondition = { deleteTime.isNull() })
        }
    }

    object UsersTable : UUIDTable("users") {
        const val SECRET_SIZE = 32
        val secret = binary("secret", length = SECRET_SIZE)
        val createTime = timestamp("create_time")
        val updateTime = timestamp("update_time")
        val deleteTime = timestamp("delete_time").nullable()
    }

    object UsernamesTable : UUIDTable("usernames") {
        const val USERNAME_SIZE = 255
        val userId = uuid("user_id").references(UsersTable.id)
        val username = varchar("username", length = USERNAME_SIZE)
        val createTime = timestamp("create_time")
        val deleteTime = timestamp("delete_time").nullable()

        init {
            uniqueIndex(username, filterCondition = { deleteTime.isNull() })
        }
    }

    object PasswordsTable : UUIDTable("passwords") {
        const val SALT_SIZE = 32
        const val HASH_SIZE = 32
        val userId = uuid("user_id").references(UsersTable.id)
        val salt = binary("salt", length = SALT_SIZE)
        val hash = binary("hash", length = HASH_SIZE)
        val createTime = timestamp("create_time")
        val deleteTime = timestamp("delete_time").nullable()
    }
}

suspend fun <T> Database.transaction(statement: suspend Transaction.() -> T) =
    newSuspendedTransaction(Dispatchers.IO, this) { statement() }

suspend fun Database.initialize(): Database {
    val version = runCatching {
        transaction {
            MetadataTable.select(MetadataTable.value)
                .where { MetadataTable.key eq "version" }
                .map { it[MetadataTable.value] }
                .singleOrNull()
        }
    }.getOrNull()

    when (version) {
        null -> createSchema()
        AuthSchema.VERSION -> {}
        else -> throw IllegalStateException("Unexpected version: $version")
    }

    return this
}

suspend fun Database.createSchema() = transaction {
    SchemaUtils.create(
        MetadataTable,
        UsersTable,
        UsernamesTable,
        PasswordsTable
    )
    MetadataTable.insert {
        it[key] = "version"
        it[value] = AuthSchema.VERSION
        it[createTime] = Clock.System.now()
    }
}
